Skip to content

feat(perps): consume perps controller analytics contract in MetaMask Mobile#33095

Open
abretonc7s wants to merge 29 commits into
mainfrom
MANUAL-000002-feat-consume-perps-controller-analy
Open

feat(perps): consume perps controller analytics contract in MetaMask Mobile#33095
abretonc7s wants to merge 29 commits into
mainfrom
MANUAL-000002-feat-consume-perps-controller-analy

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Description

Adopts the controller-owned perps analytics contract and completes the assigned tickets.

Beyond the initial contract adoption (register the five controller MetaMetrics events, entry/discovery/UTM/hlFeeRate/metamaskFee trackingData on the order lifecycle, controller search events from the market list, removal of duplicate client-side PERPS_TRADE_TRANSACTION emissions so submitted/terminal come only from the controller), this now also wires:

  • UTM merged onto all client-emitted PERPS_SCREEN_VIEWED ()
  • search query / result-tapped / abandoned with full prop sets, 500ms debounce ()
  • sort_applied / filter_applied incl. the watchlist filter
  • place_order button for ALL users incl. the deposit-required path
  • abandon_order on genuine exit — back/swipe/hardware-back and tab-switch-away, suppressed on internal TP/SL / payment-selector nav and after placement
  • screen_type=add_margin/remove_margin on margin modal open
  • watchlisted on asset_detail only
  • PERPS_TRANSACTION_CONSIDERED + PERPS_TRADE_QUOTE_RECEIVED now emitted

No SDK bump or patch required on mobile — Metro uses the correct ESM build. The published 9.2.1 CommonJS build bug is core-side and fixed in perps-controller 9.2.2 (MetaMask/core#9471; upstream nktkas/hyperliquid#165).

Deferred

  • order_execution_latency_ms wiring lands with the 9.2.2 bump (new TrackingData.orderExecutionLatencyMs).

Changelog

CHANGELOG entry: null

Related issues

Refs: MetaMask/core#9311

Manual testing steps

Feature: perps controller analytics contract consumption

  Scenario: order and search analytics use controller contract
    Given the app is running with @metamask/perps-controller 9.2.1
    And MetaMetrics is enabled

    When the user opens Perps markets and searches for a symbol
    Then Perp Search Query / Result Tapped / Abandoned events can fire from the market list

    When the user places, closes, cancels, flips, or updates TP/SL
    Then trackingData includes entryPoint/discovery fields and fee fields where available
    And the client does not emit a second Perp Trade Transaction for the same controller event

N/A for visual UI walkthrough — analytics-only change; validated via recipe state assertions + unit tests.

Screenshots/Recordings

Analytics-only change; no screenshots or video. Proof is recipe state assertions and unit tests in Validation Logs.

Validation Recipe

recipe.json (17 steps — state asserts for AC1–AC5 analytics contract consumption)
{
  "schema_version": 1,
  "title": "Consume perps controller analytics contract (MANUAL-000002)",
  "description": "Prove Mobile consumes perps-controller analytics contract: package version, MetaMetrics event registration, attribution wiring, search events, and no duplicate client trade-transaction emissions.",
  "validate": {
    "workflow": {
      "entry": "ac1-package-version",
      "nodes": {
        "ac1-package-version": {
          "action": "command",
          "intent": "AC1: confirm installed @metamask/perps-controller includes core#9311 contract",
          "proofTarget": "ac1",
          "cmd": "node -e \"const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('node_modules/@metamask/perps-controller/package.json','utf8')); const cl=fs.readFileSync('node_modules/@metamask/perps-controller/CHANGELOG.md','utf8'); if (!pkg.version.startsWith('9.')) { console.error('unexpected version', pkg.version); process.exit(1);} if (!cl.includes('#9311') && !cl.includes('PerpsAttributionContext')) { console.error('changelog missing contract markers'); process.exit(1);} console.log('ok', pkg.version);\"",
          "next": "ac1-assert-output"
        },
        "ac1-assert-output": {
          "action": "assert_output",
          "intent": "AC1: package version command printed ok",
          "proofTarget": "ac1",
          "source": "ac1-package-version",
          "stream": "stdout",
          "contains": "ok",
          "next": "ac2-search-query"
        },
        "ac2-search-query": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Search Query",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Search Query",
          "next": "ac2-search-result"
        },
        "ac2-search-result": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Search Result Tapped",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Search Result Tapped",
          "next": "ac2-search-abandoned"
        },
        "ac2-search-abandoned": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Search Abandoned",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Search Abandoned",
          "next": "ac2-considered"
        },
        "ac2-considered": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Transaction Considered",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Transaction Considered",
          "next": "ac2-quote"
        },
        "ac2-quote": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Trade Quote Received",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Trade Quote Received",
          "next": "ac3-attribution-helper"
        },
        "ac3-attribution-helper": {
          "action": "assert_file",
          "intent": "AC3: attribution helper maps entryPoint",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/utils/perpsAnalyticsAttribution.ts",
          "contains": "entryPoint",
          "next": "ac3-deeplink-utm"
        },
        "ac3-deeplink-utm": {
          "action": "assert_file",
          "intent": "AC3: deeplink handler applies UTM attribution context",
          "proofTarget": "ac3",
          "path": "app/core/DeeplinkManager/handlers/legacy/handlePerpsUrl.ts",
          "contains": "setPerpsUtmAttribution",
          "next": "ac3-order-entrypoint"
        },
        "ac3-order-entrypoint": {
          "action": "assert_file",
          "intent": "AC3: order ticket wires entry attribution helper into trackingData",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx",
          "contains": "toPerpsEntryAttribution",
          "next": "ac3-order-hl-fee"
        },
        "ac3-order-hl-fee": {
          "action": "assert_file",
          "intent": "AC3: order ticket trackingData includes hlFeeRate",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx",
          "contains": "hlFeeRate",
          "next": "ac3-close-entrypoint"
        },
        "ac3-close-entrypoint": {
          "action": "assert_file",
          "intent": "AC3: close position wires entry attribution helper into trackingData",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx",
          "contains": "toPerpsEntryAttribution",
          "next": "ac4-search-event"
        },
        "ac4-search-event": {
          "action": "assert_file",
          "intent": "AC4: market list emits controller Search Query event",
          "proofTarget": "ac4",
          "path": "app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx",
          "contains": "PERPS_SEARCH_QUERY",
          "next": "ac5-no-client-trade-dup"
        },
        "ac5-no-client-trade-dup": {
          "action": "command",
          "intent": "AC5: usePerpsOrderExecution must not emit client-side PERPS_TRADE_TRANSACTION",
          "proofTarget": "ac5",
          "cmd": "node -e \"const fs=require('fs'); const src=fs.readFileSync('app/components/UI/Perps/hooks/usePerpsOrderExecution.ts','utf8'); if (/track\\\\(\\\\s*MetaMetricsEvents\\\\.PERPS_TRADE_TRANSACTION/.test(src)) { console.error('duplicate client PERPS_TRADE_TRANSACTION still present'); process.exit(1);} console.log('no-client-trade-dup-ok');\"",
          "next": "ac5-assert-output"
        },
        "ac5-assert-output": {
          "action": "assert_output",
          "intent": "AC5: confirm no client trade-transaction duplicate emission",
          "proofTarget": "ac5",
          "source": "ac5-no-client-trade-dup",
          "stream": "stdout",
          "contains": "no-client-trade-dup-ok",
          "next": "ac3-flip-metamask-fee"
        },
        "ac3-flip-metamask-fee": {
          "action": "assert_file",
          "intent": "AC3: flip sheet passes metamaskFee for controller flip analytics",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.tsx",
          "contains": "metamaskFee",
          "next": "done"
        },
        "done": {
          "action": "end",
          "status": "pass",
          "intent": "All analytics contract consumption assertions passed"
        }
      }
    }
  }
}

Validation Logs

Command:

Full output (17/17 passed)
# MetaMask Recipe Run

Status: pass
Duration: 7.5s
Nodes: 17/17 passed

## Steps
- PASS ac1-package-version (command, 201ms): exitCode=0, stdout=ok 9.2.1

- PASS ac1-assert-output (assert_output, 93ms): source=ac1-package-version, stream=stdout, contains=ok
- PASS ac2-search-query (assert_file, 100ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac2-search-result (assert_file, 83ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac2-search-abandoned (assert_file, 69ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac2-considered (assert_file, 93ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac2-quote (assert_file, 116ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac3-attribution-helper (assert_file, 105ms): path=app/components/UI/Perps/utils/perpsAnalyticsAttribution.ts
- PASS ac3-deeplink-utm (assert_file, 143ms): path=app/core/DeeplinkManager/handlers/legacy/handlePerpsUrl.ts
- PASS ac3-order-entrypoint (assert_file, 310ms): path=app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx
- PASS ac3-order-hl-fee (assert_file, 219ms): path=app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx
- PASS ac3-close-entrypoint (assert_file, 170ms): path=app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx
- PASS ac4-search-event (assert_file, 286ms): path=app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx
- PASS ac5-no-client-trade-dup (command, 592ms): exitCode=0, stdout=no-client-trade-dup-ok

- PASS ac5-assert-output (assert_output, 366ms): source=ac5-no-client-trade-dup, stream=stdout, contains=no-client-trade-dup-ok
- PASS ac3-flip-metamask-fee (assert_file, 300ms): path=app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.tsx
- PASS done (end, 307ms)

---
mm-harness: dist/ is OLDER than src/ — this run uses the stale compiled code.
  Next: npm run build (or: trash dist/ to run straight from src via tsx)
Recipe libraries: metamask=/Users/deeeed/dev/metamask/metamask-harness/library (1 flows)
{
  "schemaVersion": 1,
  "command": "run",
  "adapter": "mobile",
  "status": "pass",
  "exitCode": 0,
  "recovered": [],
  "mutations": [],
  "reportPath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/report.md",
  "result": {
    "status": "pass",
    "summaryPath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/summary.json",
    "tracePath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/trace.json",
    "artifactManifestPath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/artifact-manifest.json",
    "recipePath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/recipe.json"
  }
}

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • N/A for this analytics-only change (validated via recipe/unit on iOS slot)
  • I've tested with a power user scenario
    • N/A — no UI/performance path change
  • I've instrumented key operations with Sentry traces for production performance metrics
    • N/A — analytics wiring only; existing Sentry paths unchanged

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Self-review fix note

Forwarded market-list source_section through market details into order trackingData discovery fields; added unit coverage for the handoff.


Note

Medium Risk
Large analytics surface across trade, search, and navigation with abandon/suppression logic; wrong events could skew product metrics but does not change order execution or funds handling.

Overview
Aligns MetaMask Mobile Perps with the controller-owned analytics contract: trade lifecycle events (PERPS_TRADE_TRANSACTION, partial fills) are no longer emitted from usePerpsOrderExecution—only trackingData is forwarded so the controller owns submitted/terminal events.

Attribution & trackingData — Adds toPerpsEntryAttribution / UTM helpers and threads entryPoint, discovery (source_section), hlFeeRate, and fee fields through order, close, cancel, flip, TP/SL, and margin flows. UTM is merged into client PERPS_SCREEN_VIEWED via usePerpsEventTracking.

New client events & funnels — Market list: debounced PERPS_SEARCH_QUERY, result taps, abandonment, sort/filter applied, and search screen views. Order screen: debounced PERPS_TRANSACTION_CONSIDERED, pay-token PERPS_TRADE_QUOTE_RECEIVED, place-order tap (all paths), and usePerpsAbandonOrderTracking for abandon_order on real exits (not child routes or after commit). Close flow gets the same abandon hook plus route buttonClicked / buttonLocation on screen views. Adjust margin screen views by mode; asset details adds watchlisted and forwards source_section into orders.

Supporting piecesderivePerpsTradeAction for considered vs executed action parity; payment-token selector dismissed tracking; navigation params extended for close entry CTAs and source_section.

Reviewed by Cursor Bugbot for commit 6c19369. Bugbot is set up for automated code reviews on this repo. Configure here.

abretonc7s and others added 3 commits July 9, 2026 14:00
Wire Mobile to the perps-controller analytics contract: register new
MetaMetrics events, propagate entry/discovery/UTM and fee fields into
trackingData, emit controller search events, and remove duplicate
client-side trade transaction emissions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Document why deeplink UTM attribution failures are swallowed so
navigation continues when Engine/controller is unavailable.

Co-authored-by: Cursor <cursoragent@cursor.com>
…002)

Pass market-list source_section through market details into order route
params and toPerpsEntryAttribution so trade trackingData includes
discoverySource fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@abretonc7s

Copy link
Copy Markdown
Contributor Author

Automated dev run — MANUAL-000002

Metric Value
Run 4124510e
Duration ?
Model cursor/grok-4.5-fast-xhigh
Nudges 0
Worker report

Description

Consume the TAT-3463 @metamask/perps-controller analytics contract in MetaMask Mobile. Register the five new controller MetaMetrics events, wire entry/discovery/UTM/hlFeeRate/metamaskFee into order lifecycle trackingData, emit controller search events from market list, and remove duplicate client-side PERPS_TRADE_TRANSACTION emissions so submitted/terminal events come only from the controller pipeline. Package already at 9.2.1 (includes core#9311) — no bump required.

Changelog

CHANGELOG entry: null

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/TAT-3463
Refs: MetaMask/core#9311

Manual testing steps

Feature: perps controller analytics contract consumption

  Scenario: order and search analytics use controller contract
    Given the app is running with @metamask/perps-controller 9.2.1
    And MetaMetrics is enabled

    When the user opens Perps markets and searches for a symbol
    Then Perp Search Query / Result Tapped / Abandoned events can fire from the market list

    When the user places, closes, cancels, flips, or updates TP/SL
    Then trackingData includes entryPoint/discovery fields and fee fields where available
    And the client does not emit a second Perp Trade Transaction for the same controller event

N/A for visual UI walkthrough — analytics-only change; validated via recipe state assertions + unit tests.

Screenshots/Recordings

N/A — analytics/contract wiring only; no visual UI change. Proof is recipe state assertions and unit tests (see Validation Logs).

Before

N/A

After

N/A

Validation Recipe

recipe.json (17 steps — state asserts for AC1–AC5 analytics contract consumption)
{
  "schema_version": 1,
  "title": "Consume perps controller analytics contract (MANUAL-000002)",
  "description": "Prove Mobile consumes TAT-3463 perps-controller analytics contract: package version, MetaMetrics event registration, attribution wiring, search events, and no duplicate client trade-transaction emissions.",
  "validate": {
    "workflow": {
      "entry": "ac1-package-version",
      "nodes": {
        "ac1-package-version": {
          "action": "command",
          "intent": "AC1: confirm installed @metamask/perps-controller includes core#9311 contract",
          "proofTarget": "ac1",
          "cmd": "node -e \"const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('node_modules/@metamask/perps-controller/package.json','utf8')); const cl=fs.readFileSync('node_modules/@metamask/perps-controller/CHANGELOG.md','utf8'); if (!pkg.version.startsWith('9.')) { console.error('unexpected version', pkg.version); process.exit(1);} if (!cl.includes('#9311') && !cl.includes('PerpsAttributionContext')) { console.error('changelog missing TAT-3463 contract markers'); process.exit(1);} console.log('ok', pkg.version);\"",
          "next": "ac1-assert-output"
        },
        "ac1-assert-output": {
          "action": "assert_output",
          "intent": "AC1: package version command printed ok",
          "proofTarget": "ac1",
          "source": "ac1-package-version",
          "stream": "stdout",
          "contains": "ok",
          "next": "ac2-search-query"
        },
        "ac2-search-query": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Search Query",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Search Query",
          "next": "ac2-search-result"
        },
        "ac2-search-result": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Search Result Tapped",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Search Result Tapped",
          "next": "ac2-search-abandoned"
        },
        "ac2-search-abandoned": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Search Abandoned",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Search Abandoned",
          "next": "ac2-considered"
        },
        "ac2-considered": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Transaction Considered",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Transaction Considered",
          "next": "ac2-quote"
        },
        "ac2-quote": {
          "action": "assert_file",
          "intent": "AC2: MetaMetrics registers Perp Trade Quote Received",
          "proofTarget": "ac2",
          "path": "app/core/Analytics/MetaMetrics.events.ts",
          "contains": "Perp Trade Quote Received",
          "next": "ac3-attribution-helper"
        },
        "ac3-attribution-helper": {
          "action": "assert_file",
          "intent": "AC3: attribution helper maps entryPoint",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/utils/perpsAnalyticsAttribution.ts",
          "contains": "entryPoint",
          "next": "ac3-deeplink-utm"
        },
        "ac3-deeplink-utm": {
          "action": "assert_file",
          "intent": "AC3: deeplink handler applies UTM attribution context",
          "proofTarget": "ac3",
          "path": "app/core/DeeplinkManager/handlers/legacy/handlePerpsUrl.ts",
          "contains": "setPerpsUtmAttribution",
          "next": "ac3-order-entrypoint"
        },
        "ac3-order-entrypoint": {
          "action": "assert_file",
          "intent": "AC3: order ticket wires entry attribution helper into trackingData",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx",
          "contains": "toPerpsEntryAttribution",
          "next": "ac3-order-hl-fee"
        },
        "ac3-order-hl-fee": {
          "action": "assert_file",
          "intent": "AC3: order ticket trackingData includes hlFeeRate",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx",
          "contains": "hlFeeRate",
          "next": "ac3-close-entrypoint"
        },
        "ac3-close-entrypoint": {
          "action": "assert_file",
          "intent": "AC3: close position wires entry attribution helper into trackingData",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx",
          "contains": "toPerpsEntryAttribution",
          "next": "ac4-search-event"
        },
        "ac4-search-event": {
          "action": "assert_file",
          "intent": "AC4: market list emits controller Search Query event",
          "proofTarget": "ac4",
          "path": "app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx",
          "contains": "PERPS_SEARCH_QUERY",
          "next": "ac5-no-client-trade-dup"
        },
        "ac5-no-client-trade-dup": {
          "action": "command",
          "intent": "AC5: usePerpsOrderExecution must not emit client-side PERPS_TRADE_TRANSACTION",
          "proofTarget": "ac5",
          "cmd": "node -e \"const fs=require('fs'); const src=fs.readFileSync('app/components/UI/Perps/hooks/usePerpsOrderExecution.ts','utf8'); if (/track\\\\(\\\\s*MetaMetricsEvents\\\\.PERPS_TRADE_TRANSACTION/.test(src)) { console.error('duplicate client PERPS_TRADE_TRANSACTION still present'); process.exit(1);} console.log('no-client-trade-dup-ok');\"",
          "next": "ac5-assert-output"
        },
        "ac5-assert-output": {
          "action": "assert_output",
          "intent": "AC5: confirm no client trade-transaction duplicate emission",
          "proofTarget": "ac5",
          "source": "ac5-no-client-trade-dup",
          "stream": "stdout",
          "contains": "no-client-trade-dup-ok",
          "next": "ac3-flip-metamask-fee"
        },
        "ac3-flip-metamask-fee": {
          "action": "assert_file",
          "intent": "AC3: flip sheet passes metamaskFee for controller flip analytics",
          "proofTarget": "ac3",
          "path": "app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.tsx",
          "contains": "metamaskFee",
          "next": "done"
        },
        "done": {
          "action": "end",
          "status": "pass",
          "intent": "All analytics contract consumption assertions passed"
        }
      }
    }
  }
}

Validation Logs

Command:

"$RUNNER_CMD" run temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe.json --adapter mobile --artifacts-dir temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run --project-root /Users/deeeed/dev/metamask/metamask-mobile-1 --slot macwork-mmdev-1 --json
Full output (17/17 passed)
# MetaMask Recipe Run

Status: pass
Duration: 7.5s
Nodes: 17/17 passed

## Steps
- PASS ac1-package-version (command, 201ms): exitCode=0, stdout=ok 9.2.1

- PASS ac1-assert-output (assert_output, 93ms): source=ac1-package-version, stream=stdout, contains=ok
- PASS ac2-search-query (assert_file, 100ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac2-search-result (assert_file, 83ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac2-search-abandoned (assert_file, 69ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac2-considered (assert_file, 93ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac2-quote (assert_file, 116ms): path=app/core/Analytics/MetaMetrics.events.ts
- PASS ac3-attribution-helper (assert_file, 105ms): path=app/components/UI/Perps/utils/perpsAnalyticsAttribution.ts
- PASS ac3-deeplink-utm (assert_file, 143ms): path=app/core/DeeplinkManager/handlers/legacy/handlePerpsUrl.ts
- PASS ac3-order-entrypoint (assert_file, 310ms): path=app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx
- PASS ac3-order-hl-fee (assert_file, 219ms): path=app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx
- PASS ac3-close-entrypoint (assert_file, 170ms): path=app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx
- PASS ac4-search-event (assert_file, 286ms): path=app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx
- PASS ac5-no-client-trade-dup (command, 592ms): exitCode=0, stdout=no-client-trade-dup-ok

- PASS ac5-assert-output (assert_output, 366ms): source=ac5-no-client-trade-dup, stream=stdout, contains=no-client-trade-dup-ok
- PASS ac3-flip-metamask-fee (assert_file, 300ms): path=app/components/UI/Perps/components/PerpsFlipPositionConfirmSheet/PerpsFlipPositionConfirmSheet.tsx
- PASS done (end, 307ms)

---
mm-harness: dist/ is OLDER than src/ — this run uses the stale compiled code.
  Next: npm run build   (or: trash dist/ to run straight from src via tsx)
Recipe libraries: metamask=/Users/deeeed/dev/metamask/metamask-harness/library (1 flows)
{
  "schemaVersion": 1,
  "command": "run",
  "adapter": "mobile",
  "status": "pass",
  "exitCode": 0,
  "recovered": [],
  "mutations": [],
  "reportPath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/report.md",
  "result": {
    "status": "pass",
    "summaryPath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/summary.json",
    "tracePath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/trace.json",
    "artifactManifestPath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/artifact-manifest.json",
    "recipePath": "/Users/deeeed/dev/metamask/metamask-mobile-1/temp/tasks/feat/manual-000002-0709-124928/artifacts/recipe-run/recipe.json"
  }
}

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • N/A for this analytics-only change (validated via recipe/unit on iOS slot)
  • I've tested with a power user scenario
    • N/A — no UI/performance path change
  • I've instrumented key operations with Sentry traces for production performance metrics
    • N/A — analytics wiring only; existing Sentry paths unchanged

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Self-review fix note

Forwarded market-list source_section through market details into order trackingData discovery fields; added unit coverage for the handoff.

@abretonc7s abretonc7s changed the title chore: prepare farmslot publication pkg-4124510e-mre3xkao feat(perps): consume perps controller analytics contract in MetaMask Mobile Jul 10, 2026
@abretonc7s abretonc7s marked this pull request as ready for review July 10, 2026 06:25
@abretonc7s abretonc7s requested review from a team as code owners July 10, 2026 06:25
…nsume-perps-controller-analy

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	app/components/UI/Perps/hooks/usePerpsOrderExecution.test.ts
#	app/components/UI/Perps/hooks/usePerpsOrderExecution.ts
Comment thread app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx Outdated
Comment thread app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx Outdated
Cancel flows now include totalFee and marketPrice so TrackingData matches the controller contract after merging main.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abretonc7s abretonc7s changed the title feat(perps): consume perps controller analytics contract in MetaMask Mobile feat(perps): consume perps controller analytics contract in MetaMask Mobile [NOT-READY] Jul 10, 2026
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🧪 Flaky unit test detection

Run history flaky detection

View recent run history

Historical failure rate is a hint, not proof — review each suggestion in context. See the flaky-test-detection skill for the full pattern reference and manual audit workflow.

Failures / runs sampled per window:

File 7d 15d 30d
app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx 0/113 0/173 0/373
app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.test.tsx 0/113 0/173 0/373
app/components/UI/Perps/Views/PerpsOrderDetailsView/PerpsOrderDetailsView.test.tsx 0/113 0/173 0/373
app/components/UI/Perps/hooks/usePerpsOrderExecution.test.ts 0/113 0/173 0/373
app/core/DeeplinkManager/handlers/legacy/__tests__/handlePerpsUrl.test.ts 0/113 0/173 0/373

AI-detected flaky patterns

app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx

  • J4 — waitFor without a real assertion inside (high)
    • Multiple waitFor callbacks use expect(queryBy*(...)).toBeDefined() as their assertion. queryBy* returns null when the element is absent — and null is not undefined, so expect(null).toBeDefined() always passes. This means the waitFor resolves immediately regardless of whether the element is actually present, turning it into a no-op synchronization barrier. Any async work that should have been awaited is silently skipped, making subsequent assertions race against pending state updates. The correct assertion for element presence is toBeOnTheScreen() (which throws when the element is absent, causing waitFor to retry until it appears or times out).
    • Suggested fix in app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx:
      -await waitFor(() => {
      -        expect(queryByTestId('perps-amount-display')).toBeDefined();
      -        expect(
      -    // ...
      +await waitFor(() => {
      +  expect(queryByTestId('perps-amount-display')).toBeOnTheScreen();
      +});
  • J4 — waitFor without a real assertion inside (high)
    • In the 'Keypad Interaction - Real Component' and 'Keypad UI Rendering' describe blocks, waitFor callbacks assert expect(queryByText('25%')).toBeDefined() and expect(queryByText('50%')).toBeDefined(). Because queryByText returns null (not undefined) when the element is absent, these assertions always pass immediately — waitFor never retries. The tests then proceed to interact with elements that may not yet be rendered, causing intermittent failures. Replace with toBeOnTheScreen() so waitFor actually polls until the element appears.
    • Suggested fix in app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx:
      -await waitFor(() => {
      -        expect(queryByText('25%')).toBeDefined();
      -        expect(queryByText('50%')).toBeDefined();
      -    // ...
      +await waitFor(() => {
      +  expect(queryByText('25%')).toBeOnTheScreen();
      +  expect(queryByText('50%')).toBeOnTheScreen();
      +});
  • J9 — Module-level mutable let bindings not reset in beforeEach (high)
    • The focusSpy variable is declared as a let binding (at describe-block or module level) and assigned inside beforeEach. If any test in the 'abandon order tracking' describe block throws before focusSpy is assigned (e.g., during renderWithProvider), the afterEach will call focusSpy.mockRestore() on the previous test's spy instance — or on undefined if it is the very first test — causing an unrelated error that masks the real failure and can make subsequent tests in the suite behave differently. Additionally, the spy is installed on the mock module object directly; if jest.clearAllMocks() in the outer beforeEach runs before the inner beforeEach re-installs the spy, there is a window where useFocusEffect is unset. The pattern is safe here because afterEach calls mockRestore(), but the uninitialized-variable risk on first run is real.
    • Suggested fix in app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx:
      -let focusSpy: jest.SpyInstance;
      -  // ...
      -  beforeEach(() => {
      -    focusSpy = jest
      -      .spyOn(jest.requireMock('@react-navigation/native'), 'useFocusEffect')
      -      .mockImplementation((...args: unknown[]) => (args[0] as () => void)());
      -  });
      -  afterEach(() => {
      -    focusSpy.mockRestore();
      -  });
      +let focusSpy: jest.SpyInstance | undefined;
      +
      +  beforeEach(() => {
      +    focusSpy = jest
      +      .spyOn(jest.requireMock('@react-navigation/native'), 'useFocusEffect')
      +      .mockImplementation((...args: unknown[]) => (args[0] as () => void)());
      +  });
      +  afterEach(() => {
      +    focusSpy?.mockRestore();
      +    focusSpy = undefined;
      +  });

app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.test.tsx

  • J9 — Module-level mutable let bindings not reset in beforeEach (high)
    • mockSearchQuery is a module-level mutable let binding that is mutated by mockSetSearchQuery and mockClearSearch. The outer beforeEach resets mockMarketDataForHook, mockWatchlistFlagEnabled, and mockRecentlyViewedFlagEnabled — but it does NOT reset mockSearchQuery back to ''. Additionally, the 'Edge Cases' test directly assigns mockSearchQuery = ' ' without going through the mock function. If any test calls mockSetSearchQuery('BTC') or directly mutates mockSearchQuery, the next test starts with a non-empty search query, causing the mock usePerpsMarketListView to filter markets unexpectedly and producing order-dependent failures.
    • Suggested fix in app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.test.tsx:35:
      -let mockSearchQuery = '';
      -const mockSetSearchQuery = jest.fn((q: string) => {
      -  mockSearchQuery = q;
      -});
      -const mockClearSearch = jest.fn(() => {
      -  mockSearchQuery = '';
      -});
      +let mockSearchQuery = '';
      +const mockSetSearchQuery = jest.fn((q: string) => {
      +  mockSearchQuery = q;
      +});
      +const mockClearSearch = jest.fn(() => {
      +  mockSearchQuery = '';
      +});
      +
      +// In the outer beforeEach, add:
      +beforeEach(() => {
      +  jest.clearAllMocks();
      +  mockSearchQuery = '';           // ← add this reset
      +  mockWatchlistFlagEnabled = false;
      +  mockRecentlyViewedFlagEnabled = false;
      +  mockMarketDataForHook.length = 0;
      +  // ... rest of beforeEach
      +});
  • J8 — jest.useFakeTimers() combined with waitFor (high)
    • Multiple tests in the 'Discovery Analytics' describe block call jest.useFakeTimers() at the start of the test body (not in beforeEach/afterEach). The afterEach restores real timers with jest.useRealTimers(). However, if any of these tests also use waitFor() while fake timers are active, waitFor's internal polling (which relies on real setTimeout) will never advance, causing the test to hang or time out intermittently. Even tests that don't use waitFor directly may be affected if the component under test triggers async state updates that waitFor would normally resolve. The per-test jest.useFakeTimers() call also means that if a previous test throws before reaching the end of the test body, the afterEach cleanup (jest.useRealTimers()) may not fully restore the timer state for subsequent tests in some Jest configurations.
    • Suggested fix in app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.test.tsx:2100:
      -it('fires search result_count after search query stabilises (debounced)', async () => {
      -  jest.useFakeTimers();
      -  mockUsePerpsMarketListView.mockReturnValue({
      -    markets: [mockMarketData[0]], // BTC only
      -    searchState: {
      +// Move fake timer setup to beforeEach/afterEach at the describe block level
      +// instead of calling jest.useFakeTimers() inside individual test bodies:
      +describe('Discovery Analytics', () => {
      +  beforeEach(() => {
      +    jest.useFakeTimers();
      +  });
      +  afterEach(() => {
      +    jest.useRealTimers();
      +  });
      +
      +  it('fires search result_count after search query stabilises (debounced)', async () => {
      +    // Remove jest.useFakeTimers() from here — it is now in beforeEach
      +    mockUsePerpsMarketListView.mockReturnValue({
      +      markets: [mockMarketData[0]],
      +      searchState: {
      +        // ...
      +      },
      +    });
      +    // Use act(() => { jest.advanceTimersByTime(N); }) instead of waitFor
      +    // when the assertion depends on a debounced timer:
      +    act(() => { jest.advanceTimersByTime(500); });
      +    expect(mockTrack).toHaveBeenCalledWith(/* ... */);
      +  });
      +});
  • J3 — Missing jest.clearAllMocks() / jest.resetAllMocks() (high)
    • The outer beforeEach calls jest.clearAllMocks(), which resets mockTrack call counts between tests. However, the 'Discovery Analytics' describe block contains tests that use mockUsePerpsMarketListView.mockReturnValue() (not mockReturnValueOnce). mockReturnValue sets a persistent return value that survives jest.clearAllMocks() (which only clears call history, not implementations). If a test sets mockUsePerpsMarketListView.mockReturnValue({...}) and the next test does not override it, the stale return value bleeds into the next test. This is especially risky for the analytics tests that rely on specific market data being returned by the hook to produce correct tracking payloads.
    • Suggested fix in app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.test.tsx:2050:
      -describe('Discovery Analytics', () => {
      -  const FILTERS_TEST_ID = PerpsMarketListViewSelectorsIDs.SORT_FILTERS;
      -  const { PERPS_EVENT_VALUE: PEV, PERPS_EVENT_PROPERTY: PEP } =
      -    jest.requireActual('@metamask/perps-controller');
      -  it('fires market_list_filter tracking with the selected category when a category badge is pressed', () => {
      -    mockWatchlistFlagEnabled = true;
      -    renderWithProvider(<PerpsMarketListView />, { state: mockState });
      -    fireEvent.press(screen.getByTestId(`${FILTERS_TEST_ID}-category-crypto`));
      -    expect(mockTrack).toHaveBeenCalledWith(
      +// After each test that uses mockReturnValue (not mockReturnValueOnce),
      +// either switch to mockReturnValueOnce, or add a resetAllMocks call:
      +beforeEach(() => {
      +  jest.clearAllMocks();
      +  jest.resetAllMocks();   // ← also resets mockReturnValue implementations
      +  mockSearchQuery = '';
      +  mockWatchlistFlagEnabled = false;
      +  mockRecentlyViewedFlagEnabled = false;
      +  mockMarketDataForHook.length = 0;
      +  // Re-establish default mock implementations after resetAllMocks:
      +  mockUseNavigation.mockReturnValue(mockNavigation);
      +  mockUseRoute.mockReturnValue({ key: 'PerpsMarketListView-123', name: 'PerpsMarketListView', params: {} });
      +  // ... other default implementations
      +});
      +// Alternatively, replace mockReturnValue with mockReturnValueOnce
      +// in each individual test so implementations don't persist.

app/components/UI/Perps/Views/PerpsOrderDetailsView/PerpsOrderDetailsView.test.tsx

  • J7 — Non-deterministic data: Date.now() in mock fixture (medium)
    • The mockOrder fixture uses Date.now() for its timestamp field. This value is evaluated once when the describe block is set up, producing a different integer on every test run. Although the current tests do not assert on the raw timestamp value (the formatOrderCardDate mock neutralises it), the non-deterministic value is still present in the object passed to mockCancelOrder and other mocks. Any future assertion that uses expect.objectContaining({ timestamp: … }) or a snapshot will fail intermittently. Pinning the timestamp to a fixed value eliminates the risk entirely and makes the fixture self-documenting.
    • Suggested fix in app/components/UI/Perps/Views/PerpsOrderDetailsView/PerpsOrderDetailsView.test.tsx:172:
      -  const mockOrder: Order = {
      -    orderId: 'order-123',
      -    symbol: 'BTC',
      -    size: '0.5',
      -    originalSize: '0.5',
      -    filledSize: '0',
      -    remainingSize: '0.5',
      -    price: '50000',
      -    side: 'buy',
      -    orderType: 'limit',
      -    timestamp: Date.now(),
      -    status: 'open',
      -    reduceOnly: false,
      -  };
      +  const mockOrder: Order = {
      +    orderId: 'order-123',
      +    symbol: 'BTC',
      +    size: '0.5',
      +    originalSize: '0.5',
      +    filledSize: '0',
      +    remainingSize: '0.5',
      +    price: '50000',
      +    side: 'buy',
      +    orderType: 'limit',
      +    timestamp: 1748217600000, // 2025-05-25T16:00:00.000Z — fixed, deterministic
      +    status: 'open',
      +    reduceOnly: false,
      +  };

app/components/UI/Perps/hooks/usePerpsOrderExecution.test.ts

  • J8 — jest.useFakeTimers() combined with waitFor (high)
    • Multiple tests in the limit orders and successful order placement describe blocks call jest.useFakeTimers() at the top of the test body and then use await waitFor(...) within the same try block while fake timers are still active. waitFor from @testing-library/react-native relies on real setTimeout for its internal polling. With fake timers active, waitFor's polling never fires, so the assertion inside never runs — the test either hangs until waitFor's wall-clock timeout (intermittent slow failure) or silently passes without actually checking the assertion (false positive). The finally block restores real timers, but only after waitFor has already been called under fake timers. The fix is to advance fake timers inside act() to trigger the async work, then assert synchronously — or restore real timers before calling waitFor.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsOrderExecution.test.ts:390:
      -it('ends as a success when a marketable limit fill fully closed the prior position', async () => {
      -  jest.useFakeTimers();
      -  try {
      -    mockGetPositionsSnapshot
      -      .mockReturnValueOnce([mockPosition]) // pre-submit baseline: a real hold
      -      // ...
      -    // ...
      -    await waitFor(() => {
      -      expect(mockEndTrace).toHaveBeenCalledWith(/* ... */);
      -    });
      -  } finally {
      -    jest.useRealTimers();
      -  }
      -});
      +it('ends as a success when a marketable limit fill fully closed the prior position', async () => {
      +  jest.useFakeTimers();
      +  try {
      +    mockGetPositionsSnapshot
      +      .mockReturnValueOnce([mockPosition]) // pre-submit baseline: a real hold
      +      // ...
      +
      +    // Advance fake timers to trigger timer-driven async work
      +    await act(async () => {
      +      await jest.runAllTimersAsync();
      +    });
      +
      +    // Assert synchronously — no waitFor needed when timers are fully drained
      +    expect(mockEndTrace).toHaveBeenCalledWith(/* ... */);
      +  } finally {
      +    jest.useRealTimers();
      +  }
      +});
  • J10 — jest.spyOn without restoreAllMocks() in afterEach (medium)
    • This test correctly uses nowSpy.mockRestore() in a finally block, so the spy is properly cleaned up for this specific test. However, the describe block has no afterEach(() => { jest.restoreAllMocks(); }) guard. If any future test in this file adds a jest.spyOn without a finally/mockRestore, or if an exception is thrown before mockRestore() is reached in a different test, the spy will leak. More importantly, the beforeEach only calls jest.clearAllMocks() (which clears call counts but does NOT restore spy implementations). Adding jest.restoreAllMocks() to afterEach provides a safety net that makes the suite robust against spy leakage regardless of test execution order or unexpected exceptions.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsOrderExecution.test.ts:340:
      -it('records a negative toast_position_delta_ms when the position renders before the toast', async () => {
      -  let clock = 1_000_000;
      -  const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => {
      -    clock += 10;
      -    return clock;
      -  });
      -  try {
      -    // ...
      -  } finally {
      -    nowSpy.mockRestore();
      -  }
      -});
      +describe('usePerpsOrderExecution', () => {
      +  // ...
      +  beforeEach(() => {
      +    jest.clearAllMocks();
      +    resetPerpsCufTraceForTests();
      +    // ...
      +  });
      +
      +  afterEach(() => {
      +    jest.restoreAllMocks(); // ← add this to guard against spy leakage
      +  });
      +
      +  // ...
      +});

app/core/DeeplinkManager/handlers/legacy/__tests__/handlePerpsUrl.test.ts

  • J8 — jest.useFakeTimers() combined with waitFor (high)
    • jest.useFakeTimers() is activated in beforeEach for every test in this file. The afterEach calls jest.runOnlyPendingTimers() then jest.useRealTimers(), which is the correct teardown pattern. However, the risk is that handlePerpsUrl is an async function that may internally use setTimeout-based logic (e.g. debouncing, retry delays). With fake timers active, any await inside the production code that depends on a real timer will hang indefinitely unless the test explicitly advances the clock. If the production code is refactored to add timer-based logic, these tests will silently hang. Additionally, if any test helper or React Native testing utility uses waitFor internally, it will conflict with the fake timers. The afterEach teardown is correct but the blanket useFakeTimers() in beforeEach is unnecessary for tests that only await a direct async function call with no timer dependencies.
    • Suggested fix in app/core/DeeplinkManager/handlers/legacy/__tests__/handlePerpsUrl.test.ts:42:
      -beforeEach(() => {
      -  jest.clearAllMocks();
      -  jest.useFakeTimers();
      -
      -  // Setup navigation mocks
      -  mockNavigate = jest.fn();
      -  mockSetParams = jest.fn();
      -  NavigationService.navigation = {
      -    navigate: mockNavigate,
      -    setParams: mockSetParams,
      -  } as unknown as typeof NavigationService.navigation;
      -
      -  // Mock DevLogger
      -  (DevLogger.log as jest.Mock) = jest.fn();
      -
      -  // Mock ReduxService.store.getState
      -  mockGetState = jest.fn();
      -  (ReduxService.store.getState as jest.Mock) = mockGetState;
      -});
      +// Remove jest.useFakeTimers() from beforeEach since handlePerpsUrl
      +// tests only await a direct async function — no timer advancement needed.
      +beforeEach(() => {
      +  jest.clearAllMocks();
      +  // jest.useFakeTimers(); ← remove this
      +
      +  // Setup navigation mocks
      +  mockNavigate = jest.fn();
      +  mockSetParams = jest.fn();
      +  NavigationService.navigation = {
      +    navigate: mockNavigate,
      +    setParams: mockSetParams,
      +  } as unknown as typeof NavigationService.navigation;
      +
      +  // Mock DevLogger
      +  (DevLogger.log as jest.Mock) = jest.fn();
      +
      +  // Mock ReduxService.store.getState
      +  mockGetState = jest.fn();
      +  (ReduxService.store.getState as jest.Mock) = mockGetState;
      +});
      +
      +// Remove afterEach timer cleanup since fake timers are no longer used:
      +// afterEach(() => {
      +//   jest.runOnlyPendingTimers();
      +//   jest.useRealTimers();
      +// });

This check is informational only and does not block merging.

@geositta geositta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for Two search correctness defects: suggested watchlist results produce inaccurate tap metrics, and unresolved result counts are recorded as zero during abandonment.

} else if (trimmedQuery) {
source_section = PERPS_EVENT_VALUE.SOURCE_SECTION.ACTIVE_SEARCH;
const resultRank =
filteredMarkets.findIndex((m) => m.symbol === market.symbol) + 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please derive resultRank and results_count from the same collection rendered to the user.

In watchlist mode, the UI combines matching watchlist markets with matching suggestions, while filteredMarkets excludes suggestions.

Tapping a suggested result therefore omits result_rank and reports an incorrect result count. A shared visibleSearchResults collection would keep rendering and analytics consistent.

: 'browse';

lastEmittedSearchQueryRef.current = normalizedQuery;
lastEmittedSearchResultsCountRef.current = resultsSettled ? resultCount : 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stale count bugbot issue in the if statement below here is resolved, but resetting this value to 0 records "no results" when results are still loading. Please store undefined when resultsSettled=false and omit results_count from PERPS_SEARCH_ABANDONED, matching PERPS_SEARCH_QUERY.

@geositta geositta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for two search correctness defects: suggested watchlist results produce inaccurate tap metrics, and search exits during loading incorrectly record results_count: 0.

geositta
geositta previously approved these changes Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePerps, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: @PerformancePreps
  • Risk Level: medium
  • AI Confidence: 88%
click to see 🤖 AI reasoning details

E2E Test Selection:
This PR is a comprehensive analytics enhancement for the Perps (perpetuals) trading feature with some functional changes:

SmokePerps (primary): The PR touches nearly every Perps view and hook:

  • PerpsOrderView: New abandon order tracking, transaction-considered events, trade quote received events, deposit confirm error handling, source_section param forwarding, hasPlacedOrderRef for commit tracking
  • PerpsClosePositionView: New abandon tracking, buttonClicked/buttonLocation route params, hasConfirmedCloseRef
  • PerpsMarketListView: Major search analytics refactor (PERPS_SEARCH_QUERY, PERPS_SEARCH_RESULT_TAPPED, PERPS_SEARCH_ABANDONED), new sort/filter tracking, visibleSearchResults memoization
  • PerpsMarketDetailsView: watchlisted property added to screen view, source_section forwarding to order screen
  • PerpsFlipPositionConfirmSheet: Additional tracking data (metamaskFee, metamaskFeeRate, protocolFeeRate, entryAttribution)
  • PerpsAdjustMarginView: New PERPS_SCREEN_VIEWED event added
  • New hooks: usePerpsAbandonOrderTracking (navigation depth tracking for abandon detection)
  • New utils: deriveTradeAction, perpsAnalyticsAttribution, perpsPaymentTokenSelection
  • handlePerpsUrl deeplink: UTM attribution parsing and storage

SmokeWalletPlatform: Per tag description, Perps is a section inside the Trending tab. Changes to PerpsMarketListView, PerpsMarketDetailsView, and other Perps views that are embedded in Trending affect this tag.

SmokeConfirmations: Changes to pay-with-modal.tsx and usePayWithCryptoSection.ts (both in the confirmations directory) add markPerpsPaymentTokenSelection() calls. These files are part of the confirmation pay-with flow used during Perps deposit-and-order transactions. The usePayWithPerpsSection hook also changed. These are functional changes to the confirmation flow, not just analytics.

Performance tests: The @PerformancePreps tag covers perps market loading, position management, add funds flow, and order execution. The PerpsOrderView and PerpsMarketListView changes include new useEffect hooks with debounced timers and memoized computations that could affect render performance. However, the changes are primarily analytics instrumentation (tracking events, refs, debounced timers) rather than core rendering path changes. The risk of performance regression is low but non-zero given the added complexity in PerpsOrderView and PerpsMarketListView.

Performance Test Selection:
The PerpsOrderView and PerpsMarketListView received significant additions: new useEffect hooks with debounced timers (1s for transaction-considered, 500ms for search query), new memoized computations (visibleSearchResults, visibleWatchlistMarkets, visibleSuggestedMarkets), and new navigation event listeners (usePerpsAbandonOrderTracking adds beforeRemove and blur listeners). These additions could affect render performance and responsiveness in the Perps market loading and order execution flows covered by @PerformancePreps.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

⚡ Performance Test Results

ℹ️ Performance test results are currently non-blocking and will not block this PR.

All tests passed · 2 tests · 1 device

📱 Devices tested (1)

Android: Google Pixel 8 Pro (v14.0)

✅ Passed Tests (2)
Test Platform Device Duration Team Recording
Perps add funds Android Google Pixel 8 Pro (v14.0) 7.79s @mm-perps-engineering-team 📹 Watch
Perps open position and close it Android Google Pixel 8 Pro (v14.0) 22.04s @mm-perps-engineering-team 📹 Watch

Branch: MANUAL-000002-feat-consume-perps-controller-analy · Build: Normal · Commit: 501dc0f · View full run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:medium AI analysis: medium risk size-XL team-money-movement issues related to Money Movement features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants